package com.edouardbataille.open
{
	import com.edouardbataille.open.meta.MetaClass;
	import com.edouardbataille.open.meta.MetaField;
	import com.edouardbataille.open.meta.MetaMany;
	import com.edouardbataille.open.requester.PreparedStatement;
	import com.edouardbataille.open.util.DeleteResponder;
	import com.edouardbataille.open.util.SaveResponder;
	import com.edouardbataille.open.util.SimpleResponder;
	import com.edouardbataille.open.util.SqlResponder;
	import com.edouardbataille.open.util.UpdateResponder;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.describeType;
	import flash.utils.getDefinitionByName;
	
	import mx.collections.ArrayCollection;
	import mx.rpc.AsyncToken;

	[Event (name="connect", type="flash.events.Event")]
	public class EntityManager extends EventDispatcher
	{
		/*
		 * Singleton methods
		 */
		private static var _instance : EntityManager = null;
		
		private var isConnect : Boolean = false;

		public function EntityManager ()
		{
			if (_instance)
				throw new Error("Singleton, should not instancied twice");
			_instance = this;
			types = new Dictionary();
		}

		public static function get instance () : EntityManager
		{
			if (!_instance)
				_instance = new EntityManager();
			return _instance;
		}

		/*
		 * End of Singleton methods
		 */

		/*
		 * private poperties
		 */
		private var types : Dictionary;

		private var con : MySqlSocket;

		/*
		 * private methods
		 */

		private function loadInfoType (clazz : Class) : void
		{
			if (types[clazz])
				return;
			types[clazz] = new MetaClass();
			var meta : MetaClass = types[clazz];
			var xml : XML = describeType(new clazz());
			var table : String = xml.metadata.(@name == "Table").arg.(@key == "name").@value;
			meta.table = table;
			meta.fields = new ArrayCollection();
			meta.many = new ArrayCollection();

			var getAllSql : String = "SELECT * ";
			var updateSQL : String = "UPDATE " + table + " SET ";
			var insertSQL : String = "INSERT INTO " + table + " (";

			parseField(meta, xml.variable);
			parseField(meta, xml.accessor);

			for each (var field : MetaField in meta.fields)
			{
				insertSQL += field.column + ", ";
				updateSQL += field.column + "=?, ";
			}
			
			if (insertSQL.lastIndexOf(", ") == insertSQL.length - 2)
				insertSQL = insertSQL.substring(0, insertSQL.length - 2);
			if (updateSQL.lastIndexOf(", ") == updateSQL.length - 2)
				updateSQL = updateSQL.substring(0, updateSQL.length - 2);
			getAllSql += " FROM " + table;
			insertSQL += ") VALUES (";
			for (var j : int = 0; j < meta.fields.length - 1; j++)
			{
				insertSQL += "?, ";
			}
			if (insertSQL.lastIndexOf(", ") == insertSQL.length - 2)
				insertSQL = insertSQL.substring(0, insertSQL.length - 2);
			insertSQL += ")";
			updateSQL += " WHERE " + types[clazz].id.column + "=?";

			var deleteSQL : String = "DELETE FROM " + table + " WHERE " + types[clazz].id.column + "=?";

			meta.getAllQuery = createStatement(getAllSql);
			meta.updateQuery = createStatement(updateSQL);
			meta.insertQuery = createStatement(insertSQL);
			meta.deleteQuery = createStatement(deleteSQL);
		}
		
		private function parseField (meta : MetaClass, xml : XMLList) : void
		{
			for (var i : int = 0; i < xml.length(); i++)
			{
				var field : MetaField = new MetaField();
				field.field = xml[i].@name.toString();
				var column : String;
				if (xml[i].metadata.(@name == "Column").length() > 0)
				{
					column = xml[i].metadata.(@name == "Column").arg.(@key == "name").@value.toString();
					field.column = column;
					meta.fields.addItem(field);
				}
				else if (xml[i].metadata.(@name == "ManyToOne").length() > 0)
				{
					var many : MetaMany = new MetaMany();
					column = xml[i].metadata.(@name == "ManyToOne").arg.(@key == "column").@value.toString()
					field.column = column;
					many.field = field;
					many.clazz = getDefinitionByName(xml[i].@type.toString ()) as Class;
					meta.many.addItem(many);
				}
				
				if (xml[i].metadata.(@name == "Id").length() > 0)
					meta.id = field;
			}
		}

		private function createStatement (queryStr : String) : PreparedStatement
		{
			var query : PreparedStatement = con.getPreparedStatement();
			query.query = queryStr;
			return query;
		}

		private function checkCon () : void
		{
			if (!con)
				throw new Error("You have to configure EntityManager");
		}
		
		private function setParameter (stmt : PreparedStatement, pos : uint, value : *) : void
		{
			var tmp : String = typeof value;
			switch (tmp)
			{
				case "int":
				case "number":
				case "uint":
					stmt.setInt(pos, value);
					break;
				case "string":
					stmt.setString(pos, value);
					break;
				case "boolean":
					stmt.setBoolean(pos, value);
					break;
				case "date":
					stmt.setDate(pos, value);
					break;
			}
		}

		public function setConfiguration (host : String, port : uint, login : String, pass : String, catalog : String) : void
		{
			con = new MySqlSocket(host, port, login, pass, catalog);
			con.addEventListener(Event.CONNECT, onConnect);
		}
		
		private function onConnect (event : Event) : void
		{
			isConnect = true;
			dispatchEvent(new Event (Event.CONNECT));
		}

		public function findAll (clazz : Class) : AsyncToken
		{
			checkCon();
			loadInfoType(clazz);
			var stmt : PreparedStatement = types[clazz].getAllQuery;
			var token : AsyncToken = stmt.execute();
			var responder : SqlResponder = new SqlResponder(clazz, types[clazz]);
			token.addResponder(responder);
			return responder.token;
		}

		public function findById (id : *, clazz : Class) : AsyncToken
		{
			checkCon();
			loadInfoType(clazz);
			var stmt : PreparedStatement = con.getPreparedStatement();
			stmt.query = "SELECT * FROM " + types[clazz].table + " WHERE " + types[clazz].id.column + "=?";
			setParameter(stmt, 0, id);
			var token : AsyncToken = stmt.execute();
			var responder : SimpleResponder = new SimpleResponder(clazz, types[clazz]);
			token.addResponder(responder);
			return responder.token;
		}

		public function findByProperty (property : String, value : *, clazz : Class) : AsyncToken
		{
			checkCon();
			loadInfoType(clazz);
			var column : String = null;
			for each (var field : MetaField in types[clazz].fields)
			{
				if (field.field == property)
				{
					column = field.column;
					break;
				}
			}
			if (!column)
				throw new Error ("Field " + property + " is not map in the class " + clazz);
			var stmt : PreparedStatement = con.getPreparedStatement();
			stmt.query = "SELECT * FROM " + types[clazz].table + " WHERE " + column + "=?";
			setParameter(stmt, 0, value);
			var token : AsyncToken = stmt.execute();
			var responder : SqlResponder = new SqlResponder(clazz, types[clazz]);
			token.addResponder(responder);
			return responder.token;
		}

		public function saveOrUpdate (obj : *, clazz : Class) : AsyncToken
		{
			checkCon();
			loadInfoType(clazz);
			var id : String = types[clazz].id.field;
			if (obj[id] == 0 || obj[id] == null)
				return save(obj, clazz);
			else
				return update(obj, clazz);
		}

		public function save (obj : *, clazz : Class) : AsyncToken
		{
			checkCon();
			loadInfoType(clazz);
			if (!(obj is clazz))
				return null;
			var stmt : PreparedStatement = types[clazz].insertQuery as PreparedStatement;
			var pos : uint = 0;
			for each (var field : MetaField in MetaClass (types[clazz]).fields)
			{
				if (field != types[clazz].id)
					setParameter(stmt, pos++, obj[field.field]);
			}
			var responder : SaveResponder = new SaveResponder (obj, clazz, types[clazz]);
			var token : AsyncToken = stmt.execute();
			token.addResponder (responder);
			return responder.token;
		}

		public function update (obj : *, clazz : Class) : AsyncToken
		{
			checkCon();
			loadInfoType(clazz);
			if (!(obj is clazz))
				return null;
			var stmt : PreparedStatement = types[clazz].updateQuery as PreparedStatement;
			var pos : uint = 0;
			for each (var field : MetaField in MetaClass (types[clazz]).fields)
			{
				if (field != types[clazz].id)
					setParameter(stmt, pos++, obj[field.field]);
			}
			setParameter(stmt, pos++, obj[types[clazz].id.field]);
			var responder : UpdateResponder = new UpdateResponder (obj, clazz, types[clazz]);
			var token : AsyncToken = stmt.execute();
			token.addResponder (responder);
			return responder.token;
		}

		public function deleteObject (obj : *, clazz : Class) : AsyncToken
		{
			checkCon();
			loadInfoType(clazz);
			var stmt : PreparedStatement = types[clazz].deleteQuery as PreparedStatement;
			setParameter(stmt, 0, obj[types[clazz].id.field]);
			var responder : DeleteResponder = new DeleteResponder (clazz, types[clazz]);
			var token : AsyncToken = stmt.execute();
			token.addResponder (responder);
			return responder.token;
		}
	}
}